﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;

namespace AxeFrog.Data
{
	public static class IDataReaderExtensions
	{
		public delegate T ObjectCreationHandler<T>();
		public delegate void SingleResultHandler<T>(T result);
		public delegate void OutAction<T, TOut>(T arg, out TOut outArg);
		public delegate void RefAction<T, TRef>(T arg, ref TRef refArg);

		/// <summary>
		/// reads each row in the current result set and calls <paramref name="action"/> for each one, passing in the data reader
		/// </summary>
		public static IDataReader ReadEach(this IDataReader dr, Action<IDataReader> action)
		{
			while(dr.Read())
				action(dr);
			return dr;
		}

		/// <summary>
		/// evaluates <paramref name="condition"/> and if true, calls <paramref name="action"/>, passing in the data reader
		/// </summary>
		public static IDataReader If(this IDataReader dr, bool condition, Action<IDataReader> action)
		{
			if(condition)
				action(dr);
			return dr;
		}

		/// <summary>
		/// calls <paramref name="action"/>, passing in the data reader
		/// </summary>
		public static IDataReader Do(this IDataReader dr, Action<IDataReader> action)
		{
			action(dr);
			return dr;
		}

		/// <summary>
		/// calls <paramref name="action"/>, passing in the data reader and <param name="arg"/>
		/// </summary>
		public static IDataReader Do<T>(this IDataReader dr, T arg, Action<IDataReader, T> action)
		{
			action(dr, arg);
			return dr;
		}

		/// <summary>
		/// Calls Read() on the data reader
		/// </summary>
		public static IDataReader ReadNext(this IDataReader dr)
		{
			dr.Read();
			return dr;
		}

		/// <summary>
		/// In a recordset with multiple results sets, this will move to the start of the next result set
		/// </summary>
		public static IDataReader NextResultSet(this IDataReader dr)
		{
			dr.NextResult();
			return dr;
		}

		/// <summary>
		/// Closes/disposes the reader and returns the passed parameter
		/// </summary>
		public static T Return<T>(this IDataReader dataReader, T returnObject)
		{
			dataReader.Close();
			dataReader.Dispose();
			return returnObject;
		}

		public static IDataReader ReadList<T>(this IDataReader dataReader, Action<List<T>> send)
		{
			List<T> list = new List<T>();
			dataReader.ReadEach(dr => list.Add(dr.MapTo<T>()));
			send(list);
			return dataReader;
		}

		public static List<T> ReadAndReturnList<T>(this IDataReader dataReader)
		{
			return dataReader.ReadAndReturnCollection<List<T>, T>(Activator.CreateInstance<T>);
		}

		public static List<T> ReadAndReturnList<T>(this IDataReader dataReader, ObjectCreationHandler<T> createObject)
		{
			return dataReader.ReadAndReturnCollection<List<T>, T>(createObject);
		}

		public static TCollection ReadAndReturnCollection<TCollection, TElement>(this IDataReader dataReader)
			where TCollection : ICollection<TElement>
		{
			return dataReader
				.ReadAndReturnCollection<TCollection, TElement>(Activator.CreateInstance<TElement>);
		}

		public static TCollection ReadAndReturnCollection<TCollection, TElement>(this IDataReader dataReader, ObjectCreationHandler<TElement> createObject)
			where TCollection : ICollection<TElement>
		{
			TCollection collection = Activator.CreateInstance<TCollection>();
			return dataReader
				.ReadEach(dr => collection.Add(dr.MapTo(createObject)))
				.Return(collection);
		}

		/// <summary>
		/// reads a row, attempts to map it into an instance type T and returns the instance after closing/disposing the reader
		/// </summary>
		public static T ReadAndReturn<T>(this IDataReader dataReader)
		{
			return dataReader.ReadAndReturn(() => Activator.CreateInstance<T>());
		}

		/// <summary>
		/// reads a row, attempts to map it into an instance type T and returns the instance after closing/disposing the reader
		/// </summary>
		/// <param name="createObject">A method that controls manual creation of the class instance that the reader will map its fields to</param>
		public static T ReadAndReturn<T>(this IDataReader dataReader, ObjectCreationHandler<T> createObject)
		{
			try
			{
				if(!dataReader.Read())
					return default(T);
				T result = dataReader.MapTo(createObject);
				return result;
			}
			finally
			{
				dataReader.Close();
				dataReader.Dispose();
			}
		}

		/// <summary>
		/// Obtains a value from the first column of the data reader and passes it to the method specified in the saveResult parameter
		/// </summary>
		public static IDataReader SaveResult<T>(this IDataReader dataReader, SingleResultHandler<T> saveResult)
		{
			saveResult(dataReader.GetResult<T>());
			return dataReader;
		}

		/// <summary>
		/// reads and maps the value in the first column of the current of the data reader to an instance of type T
		/// </summary>
		public static T GetResult<T>(this IDataReader dataReader)
		{
			T result;

			// return null/default if the datareader is invalid
			if(dataReader.FieldCount <= 0)
				result = default(T);

				// attempt to convert the single returned result (if any)
			else
				try { result = (T)Convert.ChangeType(dataReader[0], typeof(T)); }
				catch { result = default(T); }

			return result;
		}

		/// <summary>
		/// reads the existing row and maps as many columns as possible into an instance type T
		/// </summary>
		public static IDataReader MapTo<T>(this IDataReader dataReader, out T result)
		{
			result = dataReader.MapTo(() => Activator.CreateInstance<T>());
			return dataReader;
		}

		/// <summary>
		/// reads the existing row and maps as many columns as possible into an instance type T, using the passed createObject method
		/// to control instantiation of T
		/// </summary>
		public static IDataReader MapTo<T>(this IDataReader dataReader, out T result, ObjectCreationHandler<T> createObject)
		{
			result = dataReader.MapTo(createObject);
			return dataReader;
		}

		/// <summary>
		/// reads the existing row and maps as many columns as possible into an instance type T
		/// </summary>
		public static T MapTo<T>(this IDataReader dataReader)
		{
			return dataReader.MapTo(() => Activator.CreateInstance<T>());
		}

		/// <summary>
		/// reads the existing row and maps as many columns as possible into an instance type T, using the passed createObject method
		/// to control instantiation of T
		/// </summary>
		public static T MapTo<T>(this IDataReader dataReader, ObjectCreationHandler<T> createObject)
		{
			// return null/default if the datareader is invalid
			if(dataReader.FieldCount <= 0)
				return default(T);

			// if we're referring to a primitive or a string, attempt a direct conversion
			if(IsConvertibleType(typeof(T)))
				try { return (T)ConvertType(dataReader[0], typeof(T)); }
				catch { return default(T); }

			// otherwise instantiate the object type and try to fill the properties
			T newObject = createObject();
			if(EqualityComparer<T>.Default.Equals(newObject, default(T)))
				return newObject;

			// build a list of columns in the data reader
			List<string> columns = new List<string>();
			for(int i = 0; i < dataReader.FieldCount; i++)
				columns.Add(dataReader.GetName(i));

			// map any primitive or string properties to their associated values in the data reader
			foreach(var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty))
			{
				// make sure the property has an identical field name in the data reader
				var colIndex = columns.IndexOf(property.Name);
				if(colIndex > -1)
				{
					// binary data needs to be handled differently to other primitives
					if(property.PropertyType.Equals(typeof(byte[])))
					{
						int length = Convert.ToInt32(dataReader.GetBytes(colIndex, 0, null, 0, 0));
						byte[] buffer = new byte[length];
						dataReader.GetBytes(colIndex, 0, buffer, 0, length);
						property.SetValue(newObject, buffer, null);
						continue;
					}
					// handle primitives and enums (i.e. enums and things that ConvertType() can handle - ignore anything complex)
					if(IsConvertibleType(property.PropertyType))
					{
						object val = dataReader[colIndex];
						try { val = ConvertType(val, property.PropertyType); }
						catch { continue; }
						//var type = Nullable.GetUnderlyingType(property.PropertyType);
						//if(type != null && type.IsEnum)

						property.SetValue(newObject, val, null);
					}
				}
			}

			return newObject;
		}

		private static bool IsConvertibleType(Type t)
		{
			return t.IsPrimitive
				|| t.IsEnum
				|| t == typeof(double)
				|| t == typeof(string)
				|| t == typeof(DateTime)
				|| t == typeof(TimeSpan)
				|| t == typeof(Type)
				|| t.IsNullableValueType();
		}

		private static object ConvertType(object value, Type newType)
		{
			if((value ?? DBNull.Value) == DBNull.Value)
				return null;
			var type = Nullable.GetUnderlyingType(newType) ?? newType;
			if(type.IsEnum)
				return Enum.ToObject(type, Convert.ToInt32(value));
			if(type == typeof(Type))
				return Type.GetType(value as string ?? "");
			if(type == typeof(TimeSpan))
				return new TimeSpan(Convert.ToInt64(value));

			try { return Convert.ChangeType(value, type); }
			catch { return null; }
		}
	}
}